package com.liaoyifan.core.util;

import com.liaoyifan.core.valid.Lang;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.util.DigestUtils;

@SuppressWarnings("unused")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SpELKey {

    private static final ExpressionParser PARSER = new SpelExpressionParser();
    private static final String SPLIT = ":";

    public static final String PREVENT_DUPLICATE_SUBMIT = "prevent_duplicate_submit";
    public static final String IDEMPOTENT = "idempotent";
    public static final String DISTRIBUTED_LOCK = "distributed_lock";
    public static final String RATE_LIMIT = "rate_limit";

    public static StandardEvaluationContext createContext() {
        StandardEvaluationContext context = new StandardEvaluationContext();
        StandardTypeLocator typeLocator = new StandardTypeLocator();
        typeLocator.registerImport(Json.class.getPackageName());
        context.setTypeLocator(typeLocator);
        return context;
    }

    public static String preventDuplicateSubmitKey(MethodSignature signature, Object[] args) {
        String key = RequestContext.getMethodArgs(args);
        String join =
                String.join(
                        SPLIT,
                        PREVENT_DUPLICATE_SUBMIT,
                        signature.getDeclaringTypeName(),
                        signature.getName(),
                        Token.subject());
        return join
                + (key == null
                        ? ""
                        : SPLIT + DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8)));
    }

    public static String idempotentKey(MethodSignature signature, Object[] args, String spEl) {
        Object key = parseSpEL(signature.getMethod(), args, spEl);
        String baseKey =
                String.join(
                        SPLIT, IDEMPOTENT, signature.getDeclaringTypeName(), signature.getName());
        if (key instanceof Collection<?> collection) {
            if (!collection.isEmpty()) {
                return baseKey
                        + SPLIT
                        + collection.stream()
                                .map(String::valueOf)
                                .sorted()
                                .collect(Collectors.joining(","));
            }
        }
        return baseKey + SPLIT + key;
    }

    public static Set<String> distributedLockKey(
            MethodSignature signature, Object[] args, String spEl) {
        Object key = parseSpEL(signature.getMethod(), args, spEl);
        String keyPrefix = DISTRIBUTED_LOCK + SPLIT;

        if (key instanceof Collection<?> collection) {
            return collection.stream()
                    .map(item -> keyPrefix + key + SPLIT + item)
                    .collect(Collectors.toSet());
        }

        return Collections.singleton(keyPrefix + key);
    }

    public static String rateLimitKey(
            MethodSignature signature, Object[] args, String value, boolean isSpEL) {
        if (isSpEL) {
            return RATE_LIMIT + SPLIT + parseSpEL(signature.getMethod(), args, value);
        }
        return String.join(SPLIT, RATE_LIMIT, Lang.isBlank(value) ? signature.getName() : value);
    }

    private static Object parseSpEL(Method method, Object[] args, String spEL) {
        if (spEL == null || spEL.isBlank()) {
            throw new IllegalArgumentException("SpEL expression must not be blank");
        }
        StandardEvaluationContext context = createContext();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            context.setVariable(parameters[i].getName(), args[i]);
        }
        Object result = PARSER.parseExpression(spEL).getValue(context);
        if (result == null) {
            throw new IllegalArgumentException("SpEL expression parse result is null");
        }
        return result;
    }
}
